
package org.base.common.util.crypto;

import org.springframework.core.codec.CodecException;
import org.springframework.util.StringUtils;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;


public class SimpleHash {

    private static final int DEFAULT_ITERATIONS = 1;


    private final String algorithmName;


    private byte[] bytes;


    private String salt;


    private int iterations;


    private transient String base64Encoded = null;


    public SimpleHash(String algorithmName) {
        this.algorithmName = algorithmName;
        this.iterations = DEFAULT_ITERATIONS;
    }


    public SimpleHash(String algorithmName, String source) throws CodecException, NoSuchAlgorithmException {
        //noinspection NullableProblems
        this(algorithmName, source, null, DEFAULT_ITERATIONS);
    }


    public SimpleHash(String algorithmName, String source, String salt) throws CodecException, NoSuchAlgorithmException {
        this(algorithmName, source, salt, DEFAULT_ITERATIONS);
    }


    public SimpleHash(String algorithmName, String source, String salt, int hashIterations)
            throws CodecException, NoSuchAlgorithmException {
        if (!StringUtils.hasText(algorithmName)) {
            throw new NullPointerException("algorithmName argument cannot be null or empty.");
        }
        this.algorithmName = algorithmName;
        this.iterations = Math.max(DEFAULT_ITERATIONS, hashIterations);
        this.salt = salt;
        hash(source, salt, hashIterations);
    }

    private void hash(String source, String salt, int hashIterations) throws CodecException, NoSuchAlgorithmException {
        byte[] saltBytes = salt != null ? salt.getBytes() : null;
        byte[] hashedBytes = hash(source.getBytes(), saltBytes, hashIterations);
        setBytes(hashedBytes);
    }


    public String getAlgorithmName() {
        return this.algorithmName;
    }

    public String getSalt() {
        return this.salt;
    }

    public int getIterations() {
        return this.iterations;
    }

    public byte[] getBytes() {
        return this.bytes;
    }


    public void setBytes(byte[] alreadyHashedBytes) {
        this.bytes = alreadyHashedBytes;
        this.base64Encoded = null;
    }


    public void setIterations(int iterations) {
        this.iterations = Math.max(DEFAULT_ITERATIONS, iterations);
    }


    public void setSalt(String salt) {
        this.salt = salt;
    }


    protected MessageDigest getDigest(String algorithmName) throws NoSuchAlgorithmException {
        return MessageDigest.getInstance(algorithmName);
    }


    protected byte[] hash(byte[] bytes) throws NoSuchAlgorithmException {
        return hash(bytes, null, DEFAULT_ITERATIONS);
    }


    protected byte[] hash(byte[] bytes, byte[] salt) throws NoSuchAlgorithmException {
        return hash(bytes, salt, DEFAULT_ITERATIONS);
    }


    protected byte[] hash(byte[] bytes, byte[] salt, int hashIterations) throws NoSuchAlgorithmException {
        MessageDigest digest = getDigest(getAlgorithmName());
        if (salt != null) {
            digest.reset();
            digest.update(salt);
        }
        byte[] hashed = digest.digest(bytes);
        int iterations = hashIterations - 1; //already hashed once above
        //iterate remaining number:
        for (int i = 0; i < iterations; i++) {
            digest.reset();
            hashed = digest.digest(hashed);
        }
        return hashed;
    }


    public boolean isEmpty() {
        return this.bytes == null || this.bytes.length == 0;
    }


    public String toBase64() {
        if (this.base64Encoded == null) {
            //cache result in case this method is called multiple times.
            this.base64Encoded = Base64.getEncoder().encodeToString(getBytes());
        }
        return this.base64Encoded;
    }

    @Override
    public String toString() {
        return toBase64();
    }

}
